iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 6
2

https://ithelp.ithome.com.tw/upload/images/20190921/20115823Sd5NkoEShF.png

今天我們來操作一些有互動的物體 - 實體(Entity)
在Minecraft裡的實體有一些是主動的,通常都具有攻擊性,像是骷髏弓箭手、苦力怕、殭屍。而另外被動的實體,通常都是動物,像是豬、羊、牛、兔子等。

一隻豬在世界裡,會受到玩家手裡拿著胡蘿蔔而跟著,或是你可以騎著一個有裝上鞍的豬,而豬死亡時會掉下生豬肉 (或烤豬肉,端看豬有沒有著火)。如果今天豬掉下來的東西不只是自己的肉呢?

聽起來好像有些搞頭,不是嗎?

鑽石豬

在進入今天主題之前,我們先來了解實體死亡後掉落物品的事件 - LivingDeathEvent

  1. 開啟專案,在com.ithome.mymod下加入新的Class名為PigDroppingItems

  2. 在該Class內加入新的方法名為dropDiamonds,一樣加上@SubscribeEvent的注釋

  3. 接下來就是選擇要處理的事件了,這裡我們換一個方式,來讓各位一起有一個思考流程,假如你對這個沒興趣,請直接跳到下一步。
    想一想:

    • 這裡要做的是"豬死亡要掉東西"這類型的事件,我們這邊產生可能的關鍵字有pigdeaddeathdropitem

    • 有了關鍵字,在public void dropDiamonds()的括弧內,嘗試輸入上面幾個關鍵字,你會發現Intellij自動幫你找尋有可能的類別。不過很可惜的是,沒有一個關鍵字找到東西是與我們要處理的事件有關 (事件一定會有Event做結尾 - 這也是一個在設計API時可以考慮的,讓之後的開發者可以有一個規則去依循)

    • 因此我們剩下最後一種方法,從Event這個類別下手,透過Ctrl+H來尋找

    • 如下圖,在()內鍵入Event關鍵字後,按下Ctrl+H,在右手邊就會跳出所有可供使用的Event類別
      https://ithelp.ithome.com.tw/upload/images/20190918/20115823SbtkPTLych.png

      https://ithelp.ithome.com.tw/upload/images/20190918/20115823uIu2jQD7SQ.png

    • 當我們一個一個往下...看到一個EntityEvent。這時候,就要有一種感覺,我們要的事件就會在這個"實體事件"類別下

    • 果不其然,EntityEvent(實體事件) -> LivingEvent(有生命的實體事件) -> LivingDeathEvent(生物死亡事件)
      https://ithelp.ithome.com.tw/upload/images/20190918/201158239TFBWlfoRC.png

    • 找到事件類別後,開始建立我們的鑽石豬模組吧

  4. dropDiamonds內,使用LivingDeathEvent作為我們要處理的事件,完整的邏輯如下(記得import):

    @SubscribeEvent
    public void dropDiamonds(LivingDeathEvent event) {
        // 判斷這個死亡的實體是不是豬 (EntityPig)
        if(event.entity instanceof EntityPig) {
    
            // 建立隨機種子
            Random random = new Random();
            // 如果這個是server端呼叫才執行
            if(!event.entity.worldObj.isRemote) {
                // 讓豬掉下的物品變成鑽石,並且掉下的數量從0 ~ 4顆
                event.entity.dropItem(Items.diamond, random.nextInt(5));
            }
        }
    }
    
  5. 最後,在主程式內註冊我們的事件

    MinecraftForge.EVENT_BUS.register(new PigDroppingItems());
    
  6. 打開遊戲,找到豬隊友,看看這些豬身上都藏些什麼!
    https://ithelp.ithome.com.tw/upload/images/20190918/20115823CGJJo5dVMT.png

    https://ithelp.ithome.com.tw/upload/images/20190918/20115823806MwVRYeE.png
    這裡的淺綠色圓形物品就是鑽石,而另外一個則是豬肉 - 這邊可以知道,dropItem的功能並不是"取代",而是"增加"要掉落的物品。

俄羅斯豬

不知道各位有沒有聽過俄羅斯娃娃?俄羅斯娃娃是由多個一樣圖案的空心木娃娃一個套一個組成,因為是空心的木娃娃,在套的時候是從最大的依序套到最小的,而數量一般在6個以上。
在我們前面的鑽石豬模組內,我們做的事情是將豬死亡時綁定牠掉落的物品,我們想像一下:如果生成豬後會產生另一個小一點版本的豬,然後依序產生到一定數量,聽起來好像可以作為一個藝術品?

我們來著手進行吧!

  1. 在com.ithome.mymod內,加入一個新的Class名為PigDoll
  2. 第一步驟,我們處理實體加入世界的事件EntityJoinWorldEvent,並且命名為joinPigs
    @SubscribeEvent
    public void joinPigs(EntityJoinWorldEvent event) {
    }
    
  3. 一樣先回到主程式Main檔案中,將其他的事件先取消註冊,只留我們現在要處理的方法就好
    @EventHandler
    public void init(FMLInitializationEvent event) {
        MinecraftForge.EVENT_BUS.register(new PigDoll());
    }
    
  4. 回到PigDoll類別,在繼續開發之前,我們需要先思考幾個問題:
    • 要對所有的豬都產生一樣的效果嗎?還是只有我們透過生怪蛋產生的豬建立效果就好?
    • 如何只產生固定數量的豬?
    • 因為是藝術品,但是豬是會動的 (你可以拿一個胡蘿蔔試看看);如何讓豬不會動?
    • 最後,如何讓豬產生在世界裡的大小是可以改變的?

上面的每一個問題都不是很簡單,我們這邊就一步一步慢慢說明。

要對所有的豬都產生一樣的效果嗎?還是只有我們透過生怪蛋產生的豬建立效果就好?

動物在Minecraft世界裡有它自己隨機產生的機制,如果每一隻豬產生都會有一定數量的另外一群豬產生,我們會很難處理"一定數量"這件事 - 畢竟所有的豬生成都會經過EntityJoinWorldEvent這個事件!到時候你會看到世界滿滿的都是豬仔...很噁心,不要再想下去了。
因此我們只處理透過生怪蛋產生的豬 (這個物品只會在創造模式才能使用)。

如何只產生固定數量的豬?

以我們現在有處理的事件下,當生怪蛋產生豬後,會觸發EntityJoinWorldEvent事件。我們可以利用一個"全域變數"來控制豬的數量,以及利用玩家與豬的距離來判斷是否為生怪蛋"產生"的豬。

完整程式碼如下,說明直接放在註解內。

public class PigDoll {
    // AtomicInteger是多執行續安全的類別,在用全域變數時盡可能使用thread-safe的宣告
    // 我們用的是整數類別AtomicInteger去計算當前有多少豬被產生了
    // 初始值給0
    private AtomicInteger integer = new AtomicInteger(0);
    
    @SubscribeEvent
    public void joinPigs(EntityJoinWorldEvent event) {
        // 如果這個世界是客戶端,我們不做任何處理;只處理伺服器端
        if (event.entity.worldObj.isRemote) return;

        // 若實體是豬才處理
        if(event.entity instanceof EntityPig) {
            // 假如進入這個事件時玩家還未建立好,不做任何事
            if (event.world.playerEntities.isEmpty()) return;

            // 因為我們只有一個人,這裡直接透過.get(0)取得玩家就好
            EntityPlayer player = (EntityPlayer) event.world.playerEntities.get(0);
            float distance = event.entity.getDistanceToEntity(player);

            // 只處理5個方塊內的實體產生事件
            if(distance < 5.0f) {
                // 開始計數,如果當前數量已經超過5隻就不處理
                if (integer.getAndIncrement() > 5) {
                    return;
                }
            }
        }
    }
}

如何讓豬不會動?

每一個在Minecraft的實體都有自己的行為(AI),我們需要將豬的AI拿掉 (讓牠變笨),就可以達成這個目的。
我們在前一步distance的IF判斷式內放入這部分的邏輯。

// 只處理5個方塊內的實體產生事件
if(distance < 5.0f) {
    // 開始計數,如果當前數量已經超過5隻就不處理
    if (integer.getAndIncrement() > 5) {
        return;
    }

    // 建立新的豬實體
    EntityPig pig = new EntityPig(event.entity.worldObj);

    // 將豬的AI行為去除
    EntityAIBase ai = new EntityAIBase() {
        @Override
        public boolean shouldExecute() {
            return true;
        }
    };
    ai.setMutexBits(0xFFFFFF);
    pig.tasks.addTask(0, ai);
}

如何讓豬產生在世界裡的大小是可以改變的?

這一部分牽涉到要去處理Minecraft將畫面渲染(Render)的行為,而不能從實體加入到世界的事件裡面處理。因為實體的大小已經被類別 (比如我們這邊是使用EntityPig)內部定義好,並且我們真正看到的"畫面"也不是從類別內去決定牠應該要看起來多大。因此這裡我們需要額外去處理另外兩個事件:RenderLivingEvent.PreRenderLivingEvent.Post
另外要注意的是,我們會需要處理豬產生的位置,不要重疊在一起,最後完整的程式就會是:

package com.ithome.mymod;

import net.minecraft.entity.ai.EntityAIBase;
import net.minecraft.entity.passive.EntityPig;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraftforge.client.event.RenderLivingEvent;
import net.minecraftforge.event.entity.EntityJoinWorldEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.lwjgl.opengl.GL11;

import java.util.concurrent.atomic.AtomicInteger;

public class PigDoll {

    // AtomicInteger是多執行續安全的類別,在用全域變數時盡可能使用thread-safe的宣告
    // 我們用的是整數類別AtomicInteger去計算當前有多少豬被產生了
    // 初始值給0
    private AtomicInteger integer = new AtomicInteger(0);
    //初始比例變數scale
    private float scale = 1.0F;

    @SubscribeEvent
    public void preRender(RenderLivingEvent.Pre event) {
        // 是豬而且名字是myPig開頭的的才做縮小動作
        if(event.entity instanceof EntityPig && event.entity.getCustomNameTag().startsWith("myPig")) {
            // 等比例縮小並準備Render
            GL11.glPushMatrix();
            String tag = event.entity.getCustomNameTag();
            float scaleFactor = Float.parseFloat(tag.split("-")[1]);
            GL11.glTranslatef(0F, 1.5F-1.5F*scaleFactor, 0F);
            GL11.glScalef(scaleFactor, scaleFactor, scaleFactor);
        }
    }

    @SubscribeEvent
    public void postRender(RenderLivingEvent.Post event) {
        if(event.entity instanceof EntityPig && event.entity.getCustomNameTag().startsWith("myPig")) {
            // 應用Render動作
            GL11.glPopMatrix();
        }
    }

    @SubscribeEvent
    public void joinPigs(EntityJoinWorldEvent event) {
        // 如果這個世界是客戶端,我們不做任何處理;只處理伺服器端
        if (event.entity.worldObj.isRemote) return;

        // 若實體是豬才處理
        if(event.entity instanceof EntityPig) {
            // 假如進入這個事件時玩家還未建立好,不做任何事
            if (event.world.playerEntities.isEmpty()) return;

            // 因為我們只有一個人,這裡直接透過.get(0)取得玩家就好
            EntityPlayer player = (EntityPlayer) event.world.playerEntities.get(0);
            float distance = event.entity.getDistanceToEntity(player);

            // 只處理5個方塊內的實體產生事件
            if(distance < 5.0f) {
                // 開始計數,如果當前數量已經超過5隻就不處理
                if (integer.getAndIncrement() > 5) {
                    return;
                }

                // 建立新的豬實體
                EntityPig pig = new EntityPig(event.entity.worldObj);
                
                // 設定比例,並且放到自定義名字內
                scale = scale * 0.6f;
                
                // 定義我們豬的名字,在Render時可以用做篩選
                pig.setCustomNameTag(String.format("myPig-%f", scale));

                // 將豬的AI行為去除
                EntityAIBase ai = new EntityAIBase() {
                    @Override
                    public boolean shouldExecute() {
                        return true;
                    }
                };
                ai.setMutexBits(0xFFFFFF);
                pig.tasks.addTask(0, ai);

                // 每一隻豬的位置都會在前一隻豬的X軸往西方向1個方塊處
                pig.setPositionAndRotation(
                        event.entity.posX - 1.0f,
                        event.entity.posY,
                        event.entity.posZ,
                        0.0f,
                        0.0f
                );

                // 加入豬到這個世界內
                event.entity.worldObj.spawnEntityInWorld(pig);
            }
        }
    }
}

存檔後,透過創造模式進入遊戲。在一個四周圍都沒有豬的地方,裝備下圖所示的生怪蛋
https://ithelp.ithome.com.tw/upload/images/20190921/20115823mudUwpWBNS.png

使用產生豬後,你應該會看到類似下圖的結果
https://ithelp.ithome.com.tw/upload/images/20190921/20115823MCUJvDYTTr.png

第一階段達成!五隻不會動的豬隊友會在你前方從大到小依序排列。

明天我們會繼續這個功能,請各位先看看有沒有發現什麼問題?


上一篇
[Day5] 爆破也可以很有藝術
下一篇
[Day7] Minecraft Forge的事件註冊
系列文
[Minecraft - 當個創世神] 從玩遊戲到設計遊戲30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
Capillary J
iT邦新手 4 級 ‧ 2019-09-22 13:13:29

很有趣XD

不過生豬的方式為什麼用recursive的方式,而不直接就給他for迴圈5次?
這樣是不是只能丟一次蛋XD

不錯的問題~
你可以試看看,將IF的判斷改成For迴圈後,在迴圈內加上訊息印出System.out.println("hello")看看會發生什麼事XD

其實到目前為止這種將邏輯綁定在"事件"內的作法已經開始會有些問題了,包含你提到只能丟一次蛋這件事 (當然,這也有方法可以解)。
最主要的原因是 - 我們無法控制事件處理不會與另一個事件處理發生關係!

這個會留待下一篇文章來做說明

0
a31916909
iT邦新手 5 級 ‧ 2020-11-25 21:23:50

如果有很多位玩家該怎麼處理呢?
Minecraft之中的指令已經有一個內建的NBT標籤叫做:NoAI
有沒有更簡單的方法拔掉AI呢?

本系列的實作是透過Forge API來操作Minecraft世界的內容;透過NBT指令標籤來操作不在這個系列的範圍內喔~

另外,拔除AI行為的程式碼可以參考Day9

我要留言

立即登入留言